#!/usr/bin/env ruby

# is_store:
# 	if set this param equal "yes" when submit job, this board info will upload to es'index(hosts) automatically.
# 	like:
# 	- is_store: "yes"

require 'json'
require 'yaml'

def get_dmidecode_info
  info = {}
  info['id'] = ENV['HOSTNAME'] || `hostname`.chomp
  # this list comes from 'man dmidecode'
  %w[
      bios-vendor
      bios-version
      bios-release-date
      system-manufacturer
      system-version
      system-serial-number
      system-product-name
      system-family
      system-uuid
      baseboard-manufacturer
      baseboard-product-name
      baseboard-version
      baseboard-serial-number
      baseboard-asset-tag
      chassis-manufacturer
      chassis-type
      chassis-version
      chassis-serial-number
      chassis-asset-tag
      processor-family
      processor-manufacturer
      processor-version
      processor-frequency
  ].each do |dmiid|
    a = dmiid.split('-')
    b = a.shift
    c = a.join '_'
    info[b] ||= {}
    info[b][c] = %x(dmidecode -q -s #{dmiid}).chomp
    if info[b][c].include? "\n"
      info[b][c] = info[b][c].split "\n"
    end
  end
  info_to_num(info['bios'])
  info['cpu'] = info.delete 'processor'
  info
end

def get_description(line, board_info)
  line = line.gsub(/^[0-9a-z]{2}:[0-9a-z]{2}.[0-9]/, '').gsub(/\[[0-9]{4}\]/, '').gsub(/\[.{9}\]/, ':')
  descriptions = line.gsub('Ltd.', 'Ltd.:').split(':')

  board_info['description'] = descriptions[0].gsub(/^\s/, '').gsub(/\s$/, '')
  board_info['vendor'] = descriptions[1].gsub(/^\s/, '').gsub(/\s$/, '')
  board_info['product'] = descriptions[2].gsub(/^\s/, '').gsub(/\s$/, '')
end

def get_board(dev_info)
  found = false
  board_info = { 'vendorID' => '', 'deviceID' => '', 'svID' => '', 'ssID' => '' }
  dev_info.each do |line|
    if line.include?('Endpoint') && !line.include?('Root Complex Integrated')
      found = true
    elsif line.include?('Subsystem')
      index = (line =~ /[0-9a-z]{4}:[0-9a-z]{4}/)
      board_info['svID'] = line[index..index + 3]
      board_info['ssID'] = line[index + 5..index + 8]
    elsif line =~ /^[0-9a-z]{2}:[0-9a-z]{2}.[0-9]/
      index = (line =~ /[0-9a-z]{4}:[0-9a-z]{4}/)
      board_info['vendorID'] = line[index..index + 3]
      board_info['deviceID'] = line[index + 5..index + 8]
      get_description(line, board_info)
    end
  end
  return board_info if found

  return nil
end

def get_cpu_info
  lscpu = JSON.parse(%x(lscpu -J))['lscpu']
  cpu_info = {}
  lscpu.each do |i|
    k = i['field']
    v = i['data']
    next if k =~ /^On-line/
    next if k =~ /^scaling MHz/ # this number is unstable: "CPU(s) scaling MHz:  62%"
    if k =~ /^Vulnerability/
      next if v == 'Not affected'
      k.sub!(' ', '.')
    end
    k.sub!('CPU op-mode(s)', 'op-modes')
    k.sub!(/NUMA node(\d+) CPU\(s\)/, 'NUMA node\1 CPUs')
    k.sub!(/(.*)\(s\)/, 'nr_\1')
    k.gsub!(' ', '_')
    k.chomp!(':')
    k.downcase!
    cpu_info[k] = v
  end
  info_to_num(cpu_info)
  return cpu_info
end

def memory_type
  memory_type = []
  lines = {}
  memory_info = %x(dmidecode -t memory).lines
  memory_info.each do |line|
    if line.include?('Memory Device')
      memory_type.append lines if lines.length == 4
      lines = {}
    elsif line =~ /\s*Type:\s*(\S+)/
      lines['type'] = $1 if $1 != 'Unknown'
    elsif line =~ /\s*Speed:\s*(\S+)\s*(\S+)/
      lines['speed'] = $1 + $2 if $1 != 'Unknown'
    elsif line =~ /\s*Rank:\s*(\S+)/
      lines['rank'] = $1 if $1 != 'Unknown'
    elsif line =~ /\s*Serial Number:\s*(\S+)/
      lines['serial'] = $1 if $1 != 'NO DIMM'
    end
  end
  return memory_type
end

def get_memory_info
  lshw_string = %x(lshw -quiet -numeric -json).gsub(/\s{2,}/, '')
  memory_list = []
  lshw_info = JSON.parse(lshw_string)['children'][0]['children']
  lshw_info.each do |element|
    memory_list = element['children'] if element['id'] == 'memory'
  end

  all_memory = {}
  testbox = get_dmidecode_info['id']
  size = testbox.split('p-')[1].split('--')[0]
  all_memory['total_size'] = size
  memory_info = []
  memory_list.each do |mem|
    memory_type.each do |types|
      next if mem['description'].include?('empty')
      
      if types['serial'] == mem['serial']
        mem['type'] = types['type']
        mem['speed'] = types['speed']
        mem['rank'] = types['rank']
      end
      mem.delete('class')
      mem.delete('claimed')
      mem.delete('handle')
      info_to_num(mem)
      memory_info << mem
    end
  end
  all_memory['bank'] =  memory_info
  return all_memory
end

def new_card?(cards, card)
  return false unless card

  cards.each do |exist_card|
    return false if card == exist_card
  end
  return true
end

def get_cards_info
  pci_info = %x(lspci -nnv).lines
  start = 0
  cards = []
  pci_info.each_index do |index|
    next unless pci_info[index] == "\n"

    dev_info = pci_info[start..index]
    card = get_board(dev_info)
    cards << card if new_card?(cards, card)
    start = index + 1
  end
  return cards
end

def get_disk_id_map
  map = {}
  Dir.glob('/dev/disk/by-id/*').each do |id|
    name = File.basename(File.readlink(id))
    next if id.include? '/wwn-' and map.include? name
    next if id.include? '-eui.' and map.include? name
    map[name] = File.basename id
  end
  map
end

def get_disk_path_map
  map = {}
  Dir.glob('/dev/disk/by-path/*').each do |path|
    name = File.basename(File.readlink(path))
    map[name] = File.basename path
  end
  map
end

def get_facter_info(server_info)
  system('gem install facter') unless File.executable? '/usr/bin/facter'

  facter_string = %x(facter -j)
  facter_info = JSON.parse(facter_string)

  disk_name2id = get_disk_id_map
  disk_name2path = get_disk_path_map
  server_info['disks'] = []
  facter_info['disks'].each do |name, disk|
    info_to_num(disk)
    disk['device_id'] = disk_name2id[name]
    disk['device_path'] = disk_name2path[name]
    path = "/dev/disk/by-path/#{disk_name2path[name]}"
    part = %x(readlink -f #{path}).chomp
    port_type = %x(smartctl -a #{part} |grep "Transport protocol").split(':')[1]
    if port_type.nil?
      disk['transport_protocol'] = ""
    else
      disk['transport_protocol'] = port_type.lstrip.chomp
    end
    server_info['disks'].append disk
  end
  return server_info
end

def get_network
  interfaces = []
  interface = %x(nmcli dev status).lines
  interface.each do |i|
    types = {}
    next if i.include?('DEVICE')

    if i.include?('ethernet')
      interface_name = i.split(' ')[0]
      types['interface'] = interface_name.chomp
      speed = %x(ethtool #{interface_name} |grep 'Speed').split('Speed: ')[1]
      types['speed'] = speed.chomp unless speed.include?('Unknown')
      interface_type = %x(ethtool #{interface_name} |grep 'Port').split('Port:')[1]
      types['interface_type'] = interface_type.lstrip.chomp unless interface_type.nil?
      pci = %x(ethtool -i #{interface_name} |grep 'bus-info').split('bus-info: ')[1]
      types['pci'] = pci.chomp
    end
    if i.include?('disconnected')
      types['state'] = 'down'
    else
      types['state'] = 'up'
    end
    interfaces.append types if types.length == 5
  end
  return interfaces
end

def info_to_num(obj)
  return unless obj.is_a?(Hash)

  obj.each do |k, v|
    next unless v.is_a?(String)

    if v =~ /^([0-9]{1,})$/
      obj[k] = v.to_i
    elsif v =~ /^([0-9]{1,}\.[0-9]{1,})$/
      obj[k] = v.to_f
    end
  end
end

server_info = get_dmidecode_info
server_info['cpu'].update get_cpu_info
server_info['memory_info'] = get_memory_info
server_info['cards'] = get_cards_info
server_info['network'] = get_network
get_facter_info(server_info)

puts JSON.pretty_generate(server_info)
